/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jFamilyTree.provider.xml.fast;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * Provides HTML and XML entity utilities.
 * </p>
 * 
 * @see <a href="http://hotwired.lycos.com/webmonkey/reference/special_characters/">ISO Entities</a>
 * @see <a href="http://www.w3.org/TR/REC-html32#latin1">HTML 3.2 Character Entities for ISO Latin-1</a>
 * @see <a href="http://www.w3.org/TR/REC-html40/sgml/entities.html">HTML 4.0 Character entity references</a>
 * @see <a href="http://www.w3.org/TR/html401/charset.html#h-5.3">HTML 4.01 Character References</a>
 * @see <a href="http://www.w3.org/TR/html401/charset.html#code-position">HTML 4.01 Code positions</a>
 * @author <a href="mailto:alex@purpletech.com">Alexander Day Chaffee</a>
 * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
 * @since 2.0
 * @version $Id: Entities.java,v 1.1 2008/07/01 14:56:04 bbou Exp $
 */
class Entities
{
	private static final String[][] BASIC_ARRAY = { { "quot", "34" }, // " - double-quote
			{ "amp", "38" }, // & - ampersand
			{ "lt", "60" }, // < - less-than
			{ "gt", "62" }, // > - greater-than
	};

	private static final String[][] APOS_ARRAY = { { "apos", "39" }, // XML apostrophe
	};

	/**
	 * <p>
	 * The set of entities supported by standard XML.
	 * </p>
	 */
	public static final Entities XML;

	static
	{
		XML = new Entities();
		XML.addEntities(BASIC_ARRAY);
		XML.addEntities(APOS_ARRAY);
	}

	static interface EntityMap
	{
		/**
		 * <p>
		 * Add an entry to this entity map.
		 * </p>
		 * 
		 * @param name
		 *        the entity name
		 * @param value
		 *        the entity value
		 */
		void add(String name, int value);

		/**
		 * <p>
		 * Returns the value of the entity identified by the specified name.
		 * </p>
		 * 
		 * @param name
		 *        the name to locate
		 * @return entity value associated with the specified name
		 */
		int value(String name);
	}

	static abstract class MapIntMap implements EntityMap
	{
		protected Map<String, Integer> mapNameToValue;

		/**
		 * {@inheritDoc}
		 */
		public void add(String name, int value)
		{
			mapNameToValue.put(name, new Integer(value));
		}

		/**
		 * {@inheritDoc}
		 */
		public int value(String name)
		{
			Object value = mapNameToValue.get(name);
			if (value == null)
			{
				return -1;
			}
			return ((Integer) value).intValue();
		}
	}

	static class HashEntityMap extends MapIntMap
	{
		/**
		 * Constructs a new instance of <code>HashEntityMap</code>.
		 */
		public HashEntityMap()
		{
			mapNameToValue = new HashMap<String, Integer>();
		}
	}

	// package scoped for testing
	private EntityMap map = new Entities.HashEntityMap();

	/**
	 * <p>
	 * Adds entities to this entity.
	 * </p>
	 * 
	 * @param entityArray
	 *        array of entities to be added
	 */
	private void addEntities(String[][] entityArray)
	{
		for (int i = 0; i < entityArray.length; ++i)
		{
			addEntity(entityArray[i][0], Integer.parseInt(entityArray[i][1]));
		}
	}

	/**
	 * <p>
	 * Add an entity to this entity.
	 * </p>
	 * 
	 * @param name
	 *        name of the entity
	 * @param value
	 *        vale of the entity
	 */
	private void addEntity(String name, int value)
	{
		map.add(name, value);
	}

	/**
	 * <p>
	 * Returns the value of the entity identified by the specified name.
	 * </p>
	 * 
	 * @param name
	 *        the name to locate
	 * @return entity value associated with the specified name
	 */
	private int entityValue(String name)
	{
		return map.value(name);
	}

	/**
	 * <p>
	 * Unescapes the entities in a <code>String</code>.
	 * </p>
	 * <p>
	 * For example, if you have called addEntity(&quot;foo&quot;, 0xA1), unescape(&quot;&amp;foo;&quot;) will return &quot;\u00A1&quot;
	 * </p>
	 * 
	 * @param str
	 *        The <code>String</code> to escape.
	 * @return A new escaped <code>String</code>.
	 */
	public String unescape(String str)
	{
		int firstAmp = str.indexOf('&');
		if (firstAmp < 0)
		{
			return str;
		}
		else
		{
			StringWriter stringWriter = createStringWriter(str);
			try
			{
				this.doUnescape(stringWriter, str, firstAmp);
			}
			catch (IOException e)
			{
				// This should never happen because ALL the StringWriter methods called by #escape(Writer, String)
				// do not throw IOExceptions.
				return null;
			}
			return stringWriter.toString();
		}
	}

	/**
	 * Make the StringWriter 10% larger than the source String to avoid growing the writer
	 * 
	 * @param str
	 *        The source string
	 * @return A newly created StringWriter
	 */
	private StringWriter createStringWriter(String str)
	{
		return new StringWriter((int) (str.length() + (str.length() * 0.1)));
	}

	/**
	 * <p>
	 * Unescapes the escaped entities in the <code>String</code> passed and writes the result to the <code>Writer</code> passed.
	 * </p>
	 * 
	 * @param writer
	 *        The <code>Writer</code> to write the results to; assumed to be non-null.
	 * @param str
	 *        The source <code>String</code> to unescape; assumed to be non-null.
	 * @throws IOException
	 *         when <code>Writer</code> passed throws the exception from calls to the {@link Writer#write(int)} methods.
	 * @see Writer
	 */
	public void unescape(Writer writer, String str) throws IOException
	{
		int firstAmp = str.indexOf('&');
		if (firstAmp < 0)
		{
			writer.write(str);
			return;
		}
		else
		{
			doUnescape(writer, str, firstAmp);
		}
	}

	/**
	 * Underlying unescape method that allows the optimisation of not starting from the 0 index again.
	 * 
	 * @param writer
	 *        The <code>Writer</code> to write the results to; assumed to be non-null.
	 * @param str
	 *        The source <code>String</code> to unescape; assumed to be non-null.
	 * @param firstAmp
	 *        The <code>int</code> index of the first ampersand in the source String.
	 * @throws IOException
	 *         when <code>Writer</code> passed throws the exception from calls to the {@link Writer#write(int)} methods.
	 */
	private void doUnescape(Writer writer, String str, int firstAmp) throws IOException
	{
		writer.write(str, 0, firstAmp);
		int len = str.length();
		for (int i = firstAmp; i < len; i++)
		{
			char c = str.charAt(i);
			if (c == '&')
			{
				int nextIdx = i + 1;
				int semiColonIdx = str.indexOf(';', nextIdx);
				if (semiColonIdx == -1)
				{
					writer.write(c);
					continue;
				}
				int amphersandIdx = str.indexOf('&', i + 1);
				if (amphersandIdx != -1 && amphersandIdx < semiColonIdx)
				{
					// Then the text looks like &...&...;
					writer.write(c);
					continue;
				}
				String entityContent = str.substring(nextIdx, semiColonIdx);
				int entityValue = -1;
				int entityContentLen = entityContent.length();
				if (entityContentLen > 0)
				{
					if (entityContent.charAt(0) == '#')
					{ // escaped value content is an integer (decimal or
						// hexidecimal)
						if (entityContentLen > 1)
						{
							char isHexChar = entityContent.charAt(1);
							try
							{
								switch (isHexChar)
								{
								case 'X':
								case 'x':
								{
									entityValue = Integer.parseInt(entityContent.substring(2), 16);
									break;
								}
								default:
								{
									entityValue = Integer.parseInt(entityContent.substring(1), 10);
								}
								}
								if (entityValue > 0xFFFF)
								{
									entityValue = -1;
								}
							}
							catch (NumberFormatException e)
							{
								entityValue = -1;
							}
						}
					}
					else
					{ // escaped value content is an entity name
						entityValue = this.entityValue(entityContent);
					}
				}

				if (entityValue == -1)
				{
					writer.write('&');
					writer.write(entityContent);
					writer.write(';');
				}
				else
				{
					writer.write(entityValue);
				}
				i = semiColonIdx; // move index up to the semi-colon
			}
			else
			{
				writer.write(c);
			}
		}
	}

	/**
	 * <p>
	 * Unescapes a string containing XML entity escapes to a string containing the actual Unicode characters corresponding to the escapes.
	 * </p>
	 * <p>
	 * Supports only the five basic XML entities (gt, lt, quot, amp, apos). Does not support DTDs or external entities.
	 * </p>
	 * <p>
	 * Note that numerical \\u unicode codes are unescaped to their respective unicode characters. This may change in future releases.
	 * </p>
	 * 
	 * @param str
	 *        the <code>String</code> to unescape, may be null
	 * @return a new unescaped <code>String</code>, <code>null</code> if null string input
	 */
	public static String unescapeXml(String str)
	{
		if (str == null)
		{
			return null;
		}
		return Entities.XML.unescape(str);
	}
}
